#include "stdafx.h"
#include "ViewController.h"
#include <dwrite.h>


template<class T>
inline void SafeRelease(T*& p)
{
	if (p)
	{
		p->Release();
		p = nullptr;
	}
}

static inline
D2D1::ColorF GetSysColorF(int index)
{
	COLORREF c = ::GetSysColor(index);
	return D2D1::ColorF(GetRValue(c) / 255.0f, GetGValue(c) / 255.0f,
		GetBValue(c) / 255.0f);
}

static
int CalculateNumberOfHexDigits(ULONGLONG inX)
{
	int n = 0;
	if (inX >= 0x100000000LL)
	{
		n |= 8;
		inX >>= 32;
	}
	UINT32 x = UINT32(inX);
	if (x >= 0x10000)
	{
		n |= 4;
		x >>= 16;
	}
	if (x >= 0x100)
	{
		n |= 2;
		x >>= 8;
	}
	if (x >= 0x10)
	{
		n |= 1;
	}

	return (n + 1);
}


// -----------------------------------------

ViewController::ViewController()
{
	HRESULT hr = ::CoInitializeEx(NULL, COINIT_DISABLE_OLE1DDE);
	mIsPendingUninitializeComLibrary |= (S_OK == hr || S_FALSE == hr);
	mWriteFactory = nullptr;
	mWriteTextFormat = nullptr;
	mD2D1Factory = nullptr;
	mRenderTarget = nullptr;
	mIsSelectionPresent = false;

	// some initial reasonable values, they will be overridden
	mLineHeight = 18;
	mMaxWidth = 900;
}


ViewController::~ViewController()
{
	SafeRelease(mRenderTarget);
	SafeRelease(mD2D1Factory);
	SafeRelease(mWriteTextFormat);
	SafeRelease(mWriteFactory);

	if (mIsPendingUninitializeComLibrary)
	{
		::CoUninitialize();
		mIsPendingUninitializeComLibrary = false;
	}
}

long double ViewController::GetContentHeight(const FileHandler& inFileHandler) const
{
	return ((inFileHandler.GetFileLength() + 15) / 16) *
		static_cast<long double>(GetLineHeight());
}

double ViewController::GetLineHeight() const
{
	return GetRawLineHeight() * mDesktopDPIScaleY;
}

LONGLONG ViewController::GetAddressForYPosition(long double inY)
{
	LONGLONG lineNo = LONGLONG(::floor(inY / GetLineHeight()));
	return lineNo * 16;
}

long double ViewController::GetYPositionForAddress(LONGLONG inAddress)
{
	return (inAddress >> 4) * GetLineHeight();
}

int ViewController::CalculateMaxWidth(const FileHandler& inFileHandler)
{
	LONGLONG fakeLastAddress = max(1, inFileHandler.GetFileLength()) - 1;
	WCHAR text[200];
	int pos = ::swprintf_s(text, L"%.4llx: ", fakeLastAddress);
	pos += ::swprintf_s(&text[pos], 200 - pos,
		L" 00 01 02 03 04 05 06 07 - 08 09 0a 0b 0c 0d 0e 0f  0123456789abcdef");
	
	FLOAT result = 900;
	IDWriteTextLayout* textLayout = nullptr;
	HRESULT hr = mWriteFactory->CreateTextLayout(text, pos, mWriteTextFormat,
		2000, 100, &textLayout);
	if (SUCCEEDED(hr))
	{
		UINT32 actualHitTestMetricsCount = 0;
		DWRITE_HIT_TEST_METRICS hitTestMetrics[10];
		hr = textLayout->HitTestTextRange(0, pos, 0, 0, hitTestMetrics, 10,
			&actualHitTestMetricsCount);
		if (SUCCEEDED(hr) && actualHitTestMetricsCount <= 10)
		{
			if (SUCCEEDED(hr) && 0 < actualHitTestMetricsCount)
			{
				// pointer to the last element
				const DWRITE_HIT_TEST_METRICS* pMetrics = hitTestMetrics + 
					(actualHitTestMetricsCount - 1);
				result = pMetrics->left + pMetrics->width;
				mLineHeight = pMetrics->height;
			}
		}
	}
	SafeRelease(textLayout);
	mPixelsPerCharacterWidth = FLOAT(result * mDesktopDPIScaleX / pos);
	mMaxWidth = result;
	return int(::ceil(result * mDesktopDPIScaleX));
}

bool ViewController::ClearSelection()
{
	bool result = mIsSelectionPresent;
	mSelectionStart = mSelectionEnd = 0;
	mIsSelectionPresent = false;
	return result;
}

void ViewController::StartSelection(int inX, LONGLONG inY,
	const FileHandler& inFileHandler)
{
	LONGLONG address0 = LONGLONG(inY / GetLineHeight()) * 16;
	int indexOfCharacter = IndexWithinDataString(inX, address0);
	mIsSelectingHexPart = IsAtHexPart(indexOfCharacter);
	int offset = ByteOffset(indexOfCharacter);

	mSelectionStart = mSelectionEnd = min(address0 + offset,
		inFileHandler.GetFileLength() - 1);
	mIsSelectionPresent = true;
}

void ViewController::ContinueSelection(int inX, LONGLONG inY,
	const FileHandler& inFileHandler, bool inUseCurrentPart)
{
	LONGLONG address0 = LONGLONG(inY / GetLineHeight()) * 16;
	int indexOfCharacter = IndexWithinDataString(inX, address0);
	if (inUseCurrentPart)
	{
		mIsSelectingHexPart = IsAtHexPart(indexOfCharacter);
	}

	int offset = ByteOffset(indexOfCharacter);
	mSelectionEnd = min(address0 + offset,
		inFileHandler.GetFileLength() - 1);
	ExtendInvalidatedStrip();
}

void ViewController::SelectAll(const FileHandler& inFileHandler)
{
	mSelectionStart = 0;
	mSelectionEnd = inFileHandler.GetFileLength() - 1;
	mIsSelectionPresent = true;
}

void ViewController::SetSelection(LONGLONG inSelectionStart, LONGLONG inSelectionEnd)
{
	mSelectionStart = inSelectionStart;
	mSelectionEnd = inSelectionEnd;
	mIsSelectionPresent = true;
}

bool ViewController::IsSelectionPresent() const
{
	return mIsSelectionPresent;
}

void ViewController::GetSelection(LONGLONG& outSelectionStart, 
	LONGLONG& outSelectionEnd) const
{
	outSelectionStart = mSelectionStart;
	outSelectionEnd = mSelectionEnd;
}

// ------------------

void ViewController::ResetInvalidatedStrip()
{
	auto line = mSelectionEnd / 16;
	mInvalidatedStripTopLine = mInvalidatedStripBottomLine = line;
}

bool ViewController::AdjustInvalidatedRect(LONGLONG inScrollPosY, RECT& ioRect) const
{
	if (!mIsSelectionPresent)
	{
		return false;
	}
	auto lineHeight = static_cast<long double>(GetLineHeight());
	auto y0 = LONGLONG(::floor(mInvalidatedStripTopLine * lineHeight)) - 1
		- inScrollPosY;
	if (y0 >= ioRect.bottom)
	{
		return false;
	}
	if (y0 > ioRect.top)
	{
		ioRect.top = LONG(y0);
	}	

	auto y1 = LONGLONG(::ceil(mInvalidatedStripBottomLine * lineHeight)) + 1
		- inScrollPosY;
	if (y1 <= ioRect.top)
	{
		return false;
	}
	if (y1 < ioRect.bottom)
	{
		ioRect.bottom = LONG(y1);
	}
	return !::IsRectEmpty(&ioRect);
}

void ViewController::ExtendInvalidatedStrip()
{
	auto line = mSelectionEnd / 16;
	if (line <= mInvalidatedStripTopLine)
	{
		if (mInvalidatedStripTopLine == mInvalidatedStripBottomLine)
		{
			// include the height from top to bottom 
			++mInvalidatedStripBottomLine;
		}
		mInvalidatedStripTopLine = line;
	}
	else if (line >= mInvalidatedStripBottomLine)
	{
		mInvalidatedStripBottomLine = line + 1;
	}
}

// --------------------

bool ViewController::CreateDeviceIndependentResources()
{
	HRESULT hr = ::D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &mD2D1Factory);
	if (SUCCEEDED(hr))
	{
		hr = mD2D1Factory->ReloadSystemMetrics();
		if (SUCCEEDED(hr))
		{
			FLOAT dpiX, dpiY;
			mD2D1Factory->GetDesktopDpi(&dpiX, &dpiY);
			mDesktopDPIScaleX = dpiX / 96;
			mDesktopDPIScaleY = dpiY / 96;
		}
	}
	if (SUCCEEDED(hr))
	{
		HRESULT hr = ::DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
			__uuidof(IDWriteFactory), reinterpret_cast<IUnknown**>(&mWriteFactory));
		if (SUCCEEDED(hr))
		{
			hr = mWriteFactory->CreateTextFormat(L"Courier New", nullptr,
				DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL,
				DWRITE_FONT_STRETCH_NORMAL, 18, L"en-us", &mWriteTextFormat);
		}
	}
	return SUCCEEDED(hr);
}

bool ViewController::RecreateRenderTarget(HWND inHWnd)
{
	if (nullptr == mRenderTarget && nullptr != mD2D1Factory)
	{
		RECT rc;
		if (GetClientRect(inHWnd, &rc))
		{
			auto renderTargetProperties = D2D1::RenderTargetProperties();
			renderTargetProperties.usage = D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE;
			auto size = D2D1::SizeU(rc.right, rc.bottom);

			HRESULT hr = mD2D1Factory->CreateHwndRenderTarget(
				renderTargetProperties,
				D2D1::HwndRenderTargetProperties(inHWnd, size),
				&mRenderTarget);
		}
	}
	return (nullptr != mRenderTarget);
}

void ViewController::ResizeRenderTarget(int inCx, int inCy)
{
	if (mRenderTarget)
	{
		if (S_OK != mRenderTarget->Resize(D2D1::SizeU(inCx, inCy)))
		{
			mRenderTarget->Release();
			mRenderTarget = nullptr;
		}
	}
}

bool ViewController::Draw(long double inScrollPosY, const RECT& inDirtyRect,
	FileHandler& inFileHandler)
{
	// convert device pixels to points..
	D2D1_RECT_F dirtyRect;
	dirtyRect.left = FLOAT(inDirtyRect.left / mDesktopDPIScaleX);
	dirtyRect.right = FLOAT(inDirtyRect.right / mDesktopDPIScaleX);
	dirtyRect.top = FLOAT(inDirtyRect.top / mDesktopDPIScaleY);
	dirtyRect.bottom = FLOAT(inDirtyRect.bottom / mDesktopDPIScaleY);

	long double scrollPositionY = inScrollPosY / mDesktopDPIScaleY;

	auto lineNo0 = LONGLONG(floor((scrollPositionY + dirtyRect.top) / GetRawLineHeight()));
	auto lineNo1 = LONGLONG(floor((scrollPositionY + dirtyRect.bottom) / GetRawLineHeight()));
	FLOAT y = FLOAT(lineNo0 * GetRawLineHeight() - scrollPositionY);

	ID2D1SolidColorBrush* textBrush = nullptr;
	ID2D1SolidColorBrush* selectedTextBrush = nullptr;
	ID2D1SolidColorBrush* selectedBackgroundBrush = nullptr;
	HRESULT hr = mRenderTarget->CreateSolidColorBrush(
		GetSysColorF(COLOR_WINDOWTEXT), &textBrush);
	if (!SUCCEEDED(hr))
	{
		return false;
	}

	mRenderTarget->BeginDraw();
	mRenderTarget->PushAxisAlignedClip(dirtyRect, D2D1_ANTIALIAS_MODE_ALIASED);
	mRenderTarget->Clear(GetSysColorF(COLOR_WINDOW));
	
	for (auto lineNo = lineNo0; lineNo <= lineNo1;
		++lineNo, y += FLOAT(GetRawLineHeight()))
	{
		LONGLONG address = lineNo * 16;
		static const int kMaxTextLen = 200;
		WCHAR text[kMaxTextLen];
		int firstDataPosition = 0;
		int textLength = GetLineText(address, text, kMaxTextLen, inFileHandler,
			firstDataPosition);

		IDWriteTextLayout* textLayout = nullptr;
		hr = mWriteFactory->CreateTextLayout(text, textLength, mWriteTextFormat,
			mMaxWidth + 1, FLOAT(GetRawLineHeight()), &textLayout);

		if (SUCCEEDED(hr))
		{
			LONGLONG selPos0 = min(mSelectionStart, mSelectionEnd);
			LONGLONG selPos1 = max(mSelectionStart, mSelectionEnd);

			if (selPos1 >= address && selPos0 < address + 16 && mIsSelectionPresent)
			{
				++selPos1;

				//if (selPos1 + 1 < inFileHandler.GetFileLength())
				//{
				//	++selPos1;
				//}
				
				int startRelativeIndex = int(max(0, selPos0 - address));
				int endRelativeIndex = int(min(16, selPos1 - address));
				if (nullptr == selectedTextBrush)
				{
					hr = mRenderTarget->CreateSolidColorBrush(
						GetSysColorF(COLOR_HIGHLIGHTTEXT),
						&selectedTextBrush);
					if (SUCCEEDED(hr))
					{
						hr = mRenderTarget->CreateSolidColorBrush(
							GetSysColorF(COLOR_HIGHLIGHT),
							&selectedBackgroundBrush);
					}
				}
				if (SUCCEEDED(hr))
				{
					int start1 = (startRelativeIndex < 8 ? 3 * startRelativeIndex : 3 * startRelativeIndex + 2);
					int end1 = (endRelativeIndex < 8 ? 3 * endRelativeIndex : 3 * endRelativeIndex + 2);
					DWRITE_TEXT_RANGE range1 = { 
						firstDataPosition + start1,	end1 - start1
					};
					hr = HighlightBackground(textLayout, range1, y, selectedBackgroundBrush, true);
					if (SUCCEEDED(hr))
					{
						hr = textLayout->SetDrawingEffect(selectedTextBrush, range1);
					}

					DWRITE_TEXT_RANGE range2 = {
						firstDataPosition + 3 * 16 + 2 + 2 + startRelativeIndex,
						endRelativeIndex - startRelativeIndex
					};
					if (SUCCEEDED(hr))
					{
						hr = HighlightBackground(textLayout, range2, y, selectedBackgroundBrush, false);
					}
					
					if (SUCCEEDED(hr))
					{
						hr = textLayout->SetDrawingEffect(selectedTextBrush, range2);
					}
				}
			}
			mRenderTarget->DrawTextLayout({ 0, y }, textLayout, textBrush);
		}
		SafeRelease(textLayout);
	}

	mRenderTarget->PopAxisAlignedClip();
	hr = mRenderTarget->EndDraw();

	SafeRelease(textBrush);
	SafeRelease(selectedTextBrush);
	SafeRelease(selectedBackgroundBrush);
	if (D2DERR_RECREATE_TARGET == hr)
	{
		SafeRelease(mRenderTarget);
		hr = S_OK;
	}

	return SUCCEEDED(hr);
}



// -- private ---------

double ViewController::GetRawLineHeight() const
{
	return mLineHeight;
}

int ViewController::GetLineText(LONGLONG inAddress, WCHAR* outTextBuffer,
	int inBufferLength, FileHandler& inFileHandler, int& outFirstDataPosition)
{
	BYTE byteBuffer[16];
	int len = sizeof(byteBuffer);
	if (!inFileHandler.CopyBytes(inAddress, byteBuffer, len))
	{
		return 0;
	}

	int pos = swprintf_s(outTextBuffer, inBufferLength, L"%.4llx: ", inAddress);
	int firstDataPosition = pos;

	for (int i = 0; i < len; ++i)
	{
		if (8 == i)
		{
			pos += swprintf(&outTextBuffer[pos], inBufferLength - pos, L" -");
		}
		pos += swprintf_s(&outTextBuffer[pos], inBufferLength - pos, L" %02x",
			byteBuffer[i]);
	}
	int paddingCount = firstDataPosition + 2 + 3 * 16 - pos + 2;
	while (paddingCount--)
	{
		outTextBuffer[pos++] = ' ';
	}

	for (int i = 0; i < len; ++i)
	{
		char ch = byteBuffer[i];
		outTextBuffer[pos++] = (0x20 <= ch && ch < 0x7f ? ch : '.');
	}
	outTextBuffer[pos] = 0;
	outFirstDataPosition = firstDataPosition;
	return pos;
}

HRESULT ViewController::HighlightBackground(IDWriteTextLayout* ioTextLayout,
	const DWRITE_TEXT_RANGE& inRange, FLOAT inY, ID2D1Brush* inBrush,
	bool inHalfSpaceShift)
{
	DWRITE_HIT_TEST_METRICS hitTestMetrix;
	UINT32 actualHitTestMetricsCount = 0;
	HRESULT hr = ioTextLayout->HitTestTextRange(inRange.startPosition,
		inRange.length, 0, inY, &hitTestMetrix, 1, &actualHitTestMetricsCount);
	if (SUCCEEDED(hr))
	{
		FLOAT x = hitTestMetrix.left;
		FLOAT y = hitTestMetrix.top;
		if (inHalfSpaceShift)
		{
			x += 0.5f * FLOAT(mPixelsPerCharacterWidth / mDesktopDPIScaleX);
		}
		D2D1_RECT_F rect = { x, y, 
			x + hitTestMetrix.width + 1, y + hitTestMetrix.height + 1 };
		mRenderTarget->FillRectangle(rect, inBrush);
	}
	return hr;
}

int ViewController::ByteOffset(int inCharacterPosition)
{
	int result = 0;
	
	if (mIsSelectingHexPart)
	{
		int pos = max(0, inCharacterPosition);
		if (pos < 3 * 8 + 2)
		{
			result = min(7, pos / 3);
		}
		else
		{
			result = (pos - 2) / 3;
		}
	}
	else
	{
		result = inCharacterPosition - (3 * 16 + 2 + 2);
	}
	return max(0, min(15, result));
}

bool ViewController::IsAtHexPart(int inCharacterPosition)
{
	return (inCharacterPosition <= 3 * 16 + 2 + 1);
}

int ViewController::IndexWithinDataString(int inX, LONGLONG inAddress0)
{
	int firstDataPosition = max(4, ::CalculateNumberOfHexDigits(inAddress0)) + 2;
	return int(inX / mPixelsPerCharacterWidth) - firstDataPosition;
}